package blackeagle.ems.dal;

import blackeagle.ems.BlackEagleEMS;
import blackeagle.ems.Logger;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import se.oru.informatik.InformatikDB;
import se.oru.informatik.InformatikException;
import se.oru.informatik.StringHelper;

/**
 * An abstract class that simplifies the mapping of a table in a database to an object. 
 * The object to which database values will be mapped must extend the DataRecord<T> class.
 * It must also specify a table name using the DataClass-annotation and all the access methods
 * with the DataField-annotation.
 * 
 * @see blackeagle.ems.dal.DataClass
 * @see blackeagle.ems.dal.DataField
 */
public abstract class DataRecord<T extends DataRecord<T>> {
    protected Boolean _isDirty = true;
    protected Boolean _isNew = true;
    
    /**
     * Creates a new instance of the DataRecord-class
     */
    public DataRecord(){}
    
    /**
     * Finds the column - if any - marked with the DataField-annotation as being a Primary Key.
     * @return The name of the primary key column if one is found; otherwise <code>null</code>
     */
    private String getPrimaryKeyField(){
        String fieldName = null;
        Class<?> c = this.getClass();
        Method[] mx = c.getMethods();
        
        for(Method m : mx){
            Annotation a = m.getAnnotation(DataField.class);
            if(a==null)continue;
            DataField df = (DataField)a;
            if(df.fieldName().equals("")||!df.isPrimary())continue;
            
            fieldName = df.fieldName();
            break;
        }        
        
        if(fieldName==null||fieldName.isEmpty())return null;
        
        return fieldName;
    }
    
    /**
     * Fetches the value of the primary key column.
     * @return The value of the primary key column if one is found; otherwise <code>null</code>
     */
    private Object getPrimaryKeyValue(){
        Class<?> c = this.getClass();
        Method[] mx = c.getMethods();
        
        for(Method m : mx){
            Annotation a = m.getAnnotation(DataField.class);
            if(a==null)continue;
            DataField df = (DataField)a;
            if(df.fieldName().equals("")||!df.isPrimary()||df.methodType()==MethodType.Setter)continue;
            
            Object pkVal = null;
            try{
                pkVal=m.invoke(this);
            }catch(Exception e){
                Logger.log(e.getMessage());
                break;
            }
            
            return pkVal;
        }
        
        return null;
    }
    
    /**
     * Fetches the name of the table to which this class is linked.
     * @return The name of the table if the DataClass-annotation is found; otherwise <code>null</code>.
     * 
     * @see blackeagle.ems.dal.DataClass
     */
    private String getDataClassName(){
        Class<?> c = this.getClass();
        Annotation ann = c.getAnnotation(DataClass.class);
        if(ann==null)return null;
        DataClass dc = (DataClass)ann;
        if(dc.dataName().equals(""))return null;
        
        return dc.dataName();
    }
    
    /**
     * Signifies whether the record is fetched from the database or newly created.
     * @return <code>true</code> if the record is new; otherwise <code>false</code>
     */
    public Boolean isNew(){
        return this._isNew;
    }
    
    /**
     * Signifies whether the data of the record has been altered since the last save.
     * @return <code>true</code> if the data of the record has been altered; otherwise <code>false</code>
     */
    public Boolean isDirty(){
        return this._isDirty;
    }
    
    /**
     * Attempts to delete the active record. If the record isn't new it will try to delete the record from the
     * database.
     * @return <code>true</code> if the record is new or if deletion was successful; <code>false</code> otherwise.
     */
    public Boolean delete(){
        if(this._isNew)return true;
        
        InformatikDB iDB=null;
        try{
            iDB=new InformatikDB(BlackEagleEMS.connectionString);
        }catch(InformatikException e){
            Logger.log(e.getMessage());
            return false;
        }
        
        String 
                dataName = this.getDataClassName(),
                pkName = this.getPrimaryKeyField();
        Object pkVal = this.getPrimaryKeyValue();

        String query = StringHelper.sprintf("DELETE FROM # AS X WHERE X.# = '#' RETURNING X.#", dataName,pkName,pkVal,pkName);
        String s = null;
        try{
            s = iDB.fetchSingle(query);
        }catch(InformatikException e){
            Logger.log(e.getMessage());
            return false;
        }
        
        if(s!=null){
            this._isNew=true;
            return true;
        }
        
        return false;
    }
    
    /**
     * Prepares and runs an insert query based on the data contained in the record.
     * @return <code>true</code> if insertion was successful; otherwise <code>false</code>
     */
    private Boolean insertQuery(){
        InformatikDB iDB = null;
        try{
            iDB = new InformatikDB(BlackEagleEMS.connectionString);
        }catch(Exception e){
            Logger.log(e.getMessage());
            return false;
        }
        
        String dataName = this.getDataClassName();
        HashMap<String,Object> dataValues = new HashMap<String,Object>();
        Class<?> t = this.getClass();
        Method[] mx = t.getMethods();
        for(Method m : mx){
            Annotation a = m.getAnnotation(DataField.class);
            if(a==null)continue;
            DataField df = (DataField)a;
            if(df.fieldName().equals("")||df.methodType()==MethodType.Setter)continue;
            
            Object value = null;
            if(df.autoIncrement()){
                try{
                    value=iDB.getAutoIncrement(dataName, df.fieldName());
                }catch(InformatikException e){
                    Logger.log(e.getMessage());
                }
            }else{
                try{
                    value=m.invoke(this, new Object[]{});
                }catch(Exception e){
                    Logger.log(e.getMessage());
                    continue;
                }
            }
            
            dataValues.put(df.fieldName(), value);
        }
        
        StringBuilder sb = new StringBuilder();
        sb.append("INSERT INTO ");
        sb.append(dataName);
        sb.append("(");
        String[] fields = dataValues.keySet().toArray(new String[]{});
        String valStr = "";
        for(int i=0;i<fields.length;i++){
            if(i!=0){
                sb.append(", ");
                valStr+=", ";
            }
            
            sb.append(fields[i]);
            valStr+="'#'";
        }
        
        sb.append(") VALUES(");
        sb.append(valStr);
        sb.append(") RETURNING ");
        sb.append(this.getPrimaryKeyField());
        
        String query = sb.toString(),result=null;
        query = StringHelper.sprintf(query, dataValues.values().toArray());
        
        try{
            result=iDB.fetchSingle(query);
        }catch(InformatikException e){
            
        }
        
        return (result!=null);
    }
    
    /**
     * Attempts to load a record from the database. If the operation was successful, isNew and isDirty will be changed to <code>false</code>.
     * The method will try to load on the primary key of the table.
     * @param pkVal The value to load on.
     */
    public T load(Object pkVal){
        String fieldName = this.getPrimaryKeyField();
        
        return (fieldName==null?null:this.load(fieldName, pkVal));
    }
    
    /**
     * Attempts to load a record from the database. If the operation is successful, isNew and isDirty will be changed to <code>false</code>
     * @param fieldName The field name to load on.
     * @param value The value to load on.
     */
    public T load(String fieldName, Object value){
        String dataName = this.getDataClassName();
        
        InformatikDB iDB = null;
        try{
            iDB = new InformatikDB(BlackEagleEMS.connectionString);
        }catch(InformatikException e){
            return null;
        }
        if(iDB==null)return null;
        
        String query = StringHelper.sprintf("SELECT FIRST 1 * FROM # WHERE # = '#'", new Object[]{dataName,fieldName,value});
        Map<String,String> result = null;
        try{
            result = iDB.fetchRow(query);
        }catch(InformatikException e){
            return null;
        }
        
        this.fillData(result);
        
        return (T)this;
    }
    
    /**
     * Fills the record with data from a <code>Map<String,String></code>.
     * @param data The values to fill the record with.
     */
    public void fillData(Map<String,String> data){
        Method[] methods = this.getClass().getMethods();
        for(Method m : methods){
            Annotation a = m.getAnnotation(DataField.class);
            if(a==null)continue;
            DataField df = (DataField)a;
            String fn = df.fieldName();
            if(fn.equals(""))continue;
            
            if(df.methodType()==MethodType.Getter)continue;
            
            Class<?> tt = m.getParameterTypes()[0];
            
            /*
             * TODO: ? Smart casting ?
             */
            
            String val = data.get(fn);
            
            Object obj = null;
            if(tt==String.class){
                obj = val;
            }else if(tt==Integer.class){
                try{
                obj = Integer.parseInt(val);
                }catch(Exception e){
                    continue;
                }
            }else if(tt==Date.class){
                try{
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                    obj = sdf.parse(val);
                }catch(Exception e){
                    continue;
                }
            }
            
            try{
                m.invoke(this, obj);
            }catch(Exception e){
                continue;
            }
        }
            
        this._isNew=this._isDirty=false;
    }
    
    /**
     * Attempts to save the active record. If the record is new, the method will attempt to perform an INSERT
     * query. If the record isn't new, but is dirty (i.e. data has been changed since the last save), the method
     * will attempt to perform an UPDATE query.
     */
    public Boolean save(){
        if(this._isNew){
            if(this.insertQuery()){
                this._isNew=this._isDirty=false;
                return true;
            }
            
            return false;
        }else if(this._isDirty){
            if(this.updateQuery()){
                this._isDirty=false;
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Prepares and runs an update query based on the data contained in this record.
     * @return <code>true</code> if the update was successful; otherwise <code>false</code>.
     */
    private Boolean updateQuery(){
        InformatikDB iDB = null;
        try{
            iDB=new InformatikDB(BlackEagleEMS.connectionString);
        }catch(InformatikException e){
            Logger.log(e.getMessage());
            return false;
        }
        
        String pkName = this.getPrimaryKeyField();
        Object pkVal = this.getPrimaryKeyValue();
        
        StringBuilder sb = new StringBuilder();
        sb.append(StringHelper.sprintf("UPDATE # AS X SET ", this.getDataClassName()));
        
        Boolean first = true;
        Method[] mx = this.getClass().getMethods();
        for(int i=0;i<mx.length;i++){
            Method m = mx[i];
            Annotation a = m.getAnnotation(DataField.class);
            if(a==null)continue;
            DataField df = (DataField)a;
            if(df.fieldName().equals("")||df.methodType()==MethodType.Setter||df.isPrimary())continue;
            
            if(!first){
                sb.append(", ");
            }else{
                first=false;
            }
            
            Object val = null;
            try{
                val = m.invoke(this, new Object[]{});
                
                if(val instanceof Date){
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                    val = sdf.format(val);
                }
            }catch(Exception e){
                Logger.log(e.getMessage());
                continue;
            }
            
            sb.append(StringHelper.sprintf("X.# = '#'",df.fieldName(),val));
        }
        
        sb.append(StringHelper.sprintf(" WHERE X.# = '#' RETURNING X.#",pkName,pkVal,pkName));
        
        String query = sb.toString();
        String result = null;
        
        try{
            result = iDB.fetchSingle(query);
        }catch(InformatikException e){
            Logger.log(e.getMessage());
        }
        
        return (result!=null);
    }
}
